/**
 * @class 恒星实体
 * @author DuskyHuang 圣子
*/

import { _decorator, Node, Camera, MeshRenderer, Vec3, Vec4, Color, Material, renderer, ParticleSystem } from 'cc';
import { ICelestialSpecific } from '../celestial';
import { CustomizedLOD } from '../entities/customized-lod';
import { VisitProxy, cosmos, solar, ui, visit } from 'proxies';
import { CelestialLabel } from '../ui/items/celestial-label';
import { setLabelStyle, setStarAppearance } from 'helpers';
import { attenuate } from 'algorithms';
import { StarSpectrum } from 'const';
import { StarPrecise } from './star-precise';
import { StarCompanion } from './star-companion';
const { ccclass, property } = _decorator;

@ccclass('cocosmos.star')
export class Star extends CustomizedLOD implements ICelestialSpecific {

    @property(Material) public material: Material = null;
    @property(Material) public haloMaterial: Material = null;
    @property(Material) public prominenceMaterial: Material = null;
    @property(Material) public coronasMaterial: Material = null;
    @property(ParticleSystem) public particles: ParticleSystem = null;

    protected get LODValves(): num.pct[] { return STAR_LOD_VALVES };
    public readonly cat: into = 'star';
    public get config(): starConfig { return this._config; }

    public get nearest(): pow10 { return this._nearest + this._magnified; }
    public get suggest(): pow10 { return this.nearest + SUGGEST; }
    public get panorama(): pow10 { return this.nearest + 2; }
    public get farthest(): pow10 { return this._farthest; }
    public get refer(): Node { return this._precise?.node ?? null; }
    public get precise(): StarPrecise { return this._precise; }
    public get scalar(): scalar { return this._scalar; }
    public get spectrum(): StarSpectrum { return this._spectrum; }

    public get visible() { return this._visible; }
    public set visible(arg: boolean) {
        if (arg === this._visible) return;
        this.getComponentsInChildren(MeshRenderer).forEach(mesh => mesh.enabled = arg);
        this._visible = arg;
    }

    public setSurfaceColors(main: Color, spots: Color, rimLight: Color): this {
        this.material.setProperty('mainColor', main);
        this._farsee.setParams(this.suggest, Color.lerp(color, main, spots, 0.3));
        this.material.setProperty('spotColor', spots);
        this.material.setProperty('rimLightColor', rimLight);
        return this;
    }

    public setSurfaceNoisySpeeds(noisy: num.pct = 0.5, density: num.pct = 0.5, spot: num.pct = 0.5, speed: num.pct = 0.5): this {
        v4a.set(speed, noisy, speed, density);
        this._disk_angular = BaseAngular.DISK * speed;
        this.material.setProperty('args', v4a);
        this.material.setProperty('spotStrong', spot);
        return this;
    }

    public setHalo(halo: Color, scale: num.ratio = 1, density: num.ratio = 1, speed: num.ratio = 1): this {
        this.haloMaterial.setProperty('haloColor', halo);
        v4b.set(density, density, speed, speed);
        this.haloMaterial.setProperty('args', v4b);
        const s = BaseScales.HALO * scale;
        this._halo.setScale(s, s, s);
        return this;
    }

    public setProminence(prom: Color, scale: num.ratio = 1): this {
        this.prominenceMaterial.setProperty('mainColor', prom);
        const s = BaseScales.PROMINENCE * scale;
        this._prominences.setScale(s, s, s);
        return this;
    }

    public setCoronas(corona: Color, speed: num.ratio = 1): this {
        this.coronasMaterial.setProperty('mainColor', corona);
        this._coronas_angular = BaseAngular.CORONA * speed;
        return this;
    }

    public useDiskParticles(use: false): this;
    public useDiskParticles(use: true, disk?: Color, scale?: num.ratio, attenuation?: attenuation): this;
    public useDiskParticles(use: boolean, disk?: Color, scale: num.ratio = 1, attenuation?: attenuation): this {
        if (!this._disk) return;
        this._disk.active = use;
        if (use) {
            if (disk) {
                this._diskMaterial.setProperty('startColor', disk);
                this._diskMaterial.setProperty('fadeColor', disk);
                this.particles.clear();
                this.particles.startColor.colorMin = disk;
                this.particles.startColor.colorMax = Color.WHITE;
                this.particles.play();
            }
            if (isNum(scale)) {
                const s = BaseScales.DISK * scale;
                this._disk.setScale(s, s, s);
            } else this._disk.setScale(1, 1, 1);
            if (notEmptyArr(attenuation)) {
                const [x, y, z, w] = attenuation;
                this._diskMaterial.setProperty('attenuation', v4c.set(x, y, z, w));
            } else this._diskMaterial.setProperty('attenuation', ACCRETION_DISK);
        }
        return this;
    }

    protected update(dt: time.sec): void {
        if (this._coronas) {
            this.faceToView(this._coronas);
            this._coronas.children.forEach((child, i) =>
                child.angle += dt * this._coronas_angular * (0.5 - i));
        }
        if (this._farsee?.prominence) {
            this.faceToView(this._farsee?.prominence);
        }
        if (this._disk && this._disk.activeInHierarchy) {
            const { y } = this._disk.eulerAngles;
            this._disk.setRotationFromEuler(0, dt * this._disk_angular + y, 0);
        }
    }

    public faceToView(child: Node): void {
        child.forward = Vec3.subtract(v3a, this.node.worldPosition, this._viewer.node.worldPosition);
    }

    public loadConfig(config: starConfig, viewer: Camera, base: pow10): void {
        this._config = config;
        this._viewer = viewer;
        const { diam, dist, xyz } = config;
        const [x, y, z] = xyz ?? [1, 1, 1];
        this._real.set(diam * x, diam * y, diam * z);
        this.updateScale();
        this._nearest = Math.log10(diam) + base;
        this._farthest = Math.log10(dist * STAR_FAR_RATIO);
        this._spectrum = StarSpectrum[config.spec.charAt(0).toUpperCase()];
        setStarAppearance(this);
    }

    public loadAsCompanion(viewer: Camera, diam: ly): void {
        this._viewer = viewer;
        this._real.set(diam, diam, diam);
        this.node.setScale(this._real);
    }

    public loadReference(precise: StarPrecise): void {
        this._precise = precise;
    }

    private _config: starConfig;
    private _visible: boolean = false;
    private _nearest: pow10 = 0;
    private _farthest: pow10 = 0;
    private _precise: StarPrecise = null;
    private _scalar: scalar = 'Stellar';
    private _magnified: pow10 = 0;
    private _spectrum: StarSpectrum = StarSpectrum.G;
    private _diskMaterial: renderer.MaterialInstance = null;

    protected init(): void {
        this._halo = this._lod0.getChildByName('Halo');
        this._coronas = this._lod0.getChildByName('Coronas');
        this._prominences = this._lod0.getChildByName('Prominences');
        this._disk = this._lod0.getChildByName('Disk');
        this._diskMaterial = this._disk?.getComponent(MeshRenderer).getMaterialInstance(0);
        this._farsee = this.node.getChildByName('Farsee').getComponent(StarCompanion);
    }

    protected onLod0Initiated(): void { }
    protected onTick(): void {
        if (visit.inSolar && this.node.name === "Sun") {
            this._magnified = Math.log10(solar.getMagnified(this.cat));
        }
        if (this._config) this.syncNameLabel();
    }

    protected syncNameLabel(): void {
        if (this.node.activeInHierarchy && ui.showCelNames) {
            if (!this._label) {
                this._label = setLabelStyle(CelestialLabel.claim('solar', this.node.name), 'solar', 'star');
                this._label.camera = this._viewer;
                this._label.target = this.node;
                const { name, cname, post } = this._config;
                const _post = post ? (' ' + post) : '';
                this._label.config = {
                    en: name + _post,
                    cn: cname + _post,
                }
                this._label.setUIOffset(0, 30);
            }
            this._label.alpha = attenuate(cosmos.pow10, STAR_LABEL_ATT, false);
        } else this._freeLabel();
    }

    private _freeLabel(): void {
        if (this._label) {
            this._label.free();
            this._label = null;
        }
    }

    protected updateScale(magnified?: num.pct): void {
        if (this.node.name === 'Sun') super.updateScale(magnified);
        else this.node.setScale(this._real);
    }

    private _label: CelestialLabel = null;
    private _halo: Node;
    private _prominences: Node;
    private _coronas: Node;
    private _disk: Node;
    private _disk_angular: num.deg = BaseAngular.DISK;
    private _coronas_angular: num.deg = BaseAngular.CORONA;
    private _farsee: StarCompanion;
}

const STAR_LABEL_ATT: attenuation = [-6.5, -5.5, -2.5, -1.5];
const { LensOffsets: { SUGGEST } } = VisitProxy;
const STAR_LOD_VALVES = [1];
const STAR_FAR_RATIO = 2.2;
const BaseScales = {
    HALO: 1.8,
    PROMINENCE: 5,
    CORONA: 1.56,
    DISK: 0.5,
}
const BaseAngular = {
    CORONA: 10,
    DISK: 5,
}
const v3a = new Vec3;
const v4a = new Vec4;
const v4b = new Vec4;
const v4c = new Vec4;
const color = new Color;
const ACCRETION_DISK = new Vec4(0.2, 0.7, 0.7, 1);